/*
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
  runNextTick,
  bitfield,
  // bitfields
  kObjectMode,
  kErrorEmitted,
  kAutoDestroy,
  kEmitClose,
  kDestroyed,
  kClosed,
  kCloseEmitted,
  kErrored,
  kConstructed,
  Stream,
} from './stream';
import buffer from '@ohos.buffer';

const kSync: number = 1 << 9;
const kFinalCalled: number = 1 << 10;
const kNeedDrain: number = 1 << 11;
const kEnding: number = 1 << 12;
const kFinished: number = 1 << 13;
const kDecodeStrings: number = 1 << 14;
const kWriting: number = 1 << 15;
const kBufferProcessing: number = 1 << 16;
const kPrefinished: number = 1 << 17;
const kAllNoop: number = 1 << 19;
const kOnFinished: number = 1 << 20;
const kHasWritable: number = 1 << 21;
const kWritable: number = 1 << 22;
const kCorked: number = 1 << 23;
const kDefaultUTF8Encoding: number = 1 << 24;
const kWriteCb: number = 1 << 25;
const kExpectWriteCb: number = 1 << 26;
const kAfterWriteTickInfo: number = 1 << 27;
const kAfterWritePending: number = 1 << 28;
const kBuffered: number = 1 << 29;
const kEnded: number = 1 << 30;

const buffers = Symbol('buffersSymbol');

type chunk = string | buffer.Buffer | Uint8Array | any;
type wrappedChunk = {
  chunk: string | buffer.Buffer;
  encoding: string | null;
  callback?: Function;
} | any;
type packedWritecbInfo = {
  count: number;
  callback: Function;
}

class WritableState {
  [bitfield]: number;
  [buffers]?: wrappedChunk[];
  highWaterMark: number;
  length: number = 0;
  currentBufferIndex: number = 0;
  currentWritelen: number = 0;
  currentWritecb?: Function;
  packedWritecbInfo?: packedWritecbInfo;
  pendingcb: number = 0;
  error?: Error;
  onwrite: Function;
  constructor(stream: Writable) {
    this[bitfield] = 0;
    this.onwrite = onwrite.bind(undefined, stream, this);
  }
}

function onwrite(stream: Writable, state: WritableState, error?: Error): void {
  if ((state[bitfield] & kExpectWriteCb) === 0) {
    errorOrDestroy(stream, new ERR_MULTIPLE_CALLBACK());
    return;
  }

  const sync = (state[bitfield] & kSync) !== 0;
  const callback = state.currentWritecb;

  state[bitfield] &= ~(kWriting | kExpectWriteCb | kWriteCb);
  state.length -= state.currentWritelen;
  state.currentWritelen = 0;

  if (error) {
    if ((state[bitfield] & kErrored) === 0) {
      state[bitfield] |= kErrored;
      state.error = error;
    }

    // In case of duplex streams we need to notify the readable side of the
    // error.
    if (stream._readableState && !stream._readableState.error) {
      stream._readableState.error = error;
    }

    if (sync) {
      runNextTick(onwriteError, stream, error, callback);
    } else {
      onwriteError(stream, error, callback);
    }
  } else {
    if ((state[bitfield] & kBuffered) !== 0) {
      writeAllBuffer(stream, state);
    }

    if (sync) {
      const needDrain = (state[bitfield] & kNeedDrain) !== 0 && state.length === 0;
      const needTick = needDrain || (state[bitfield] & kDestroyed !== 0) || callback !== undefined;

      // It is a common case that the callback passed to .write() is always
      // the same. In that case, we do not schedule a new nextTick(), but
      // rather just increase a counter, to improve performance and avoid
      // memory allocations.
      if (callback === undefined) {
        if ((state[bitfield] & kAfterWritePending) === 0 && needTick) {
          runNextTick(afterWrite, stream, 0);
          state[bitfield] |= kAfterWritePending;
        } else {
          state.pendingcb--;
          if ((state[bitfield] & kEnding) !== 0) {
            finishMaybe(stream, true);
          }
        }
      } else if (state.packedWritecbInfo?.callback === callback) {
        state.packedWritecbInfo.count++;
      } else if (needTick) {
        state.packedWritecbInfo = {count: 1, callback};
        runNextTick(afterWriteTick, state.packedWritecbInfo);
        state[bitfield] |= (kAfterWritePending | kAfterWriteTickInfo);
      } else {
        state.pendingcb--;
        if ((state[bitfield] & kEnding) !== 0) {
          finishMaybe(stream, state, true);
        }
      }
    } else {
      afterWrite(stream, state, 1, callback);
    }
  }
}

function afterWrite(stream: Writable, state: WritableState, count: number, callback?: Function): void {
  state[bitfield] &= ~kAfterWritePending;

  const needDrain = (state[bitfield] & (kEnding | kNeedDrain | kDestroyed)) === kNeedDrain && state.length === 0;
  if (needDrain) {
    state[bitfield] &= ~kNeedDrain;
    stream.emit('drain');
  }

  if (callback) {
    while (count-- > 0) {
      callback(null);
    }
  }

  if ((state[bitfield] & kDestroyed) !== 0) {
    errorBuffer(state);
  }

  if ((state[bitfield] & kEnding) !== 0) {
    finishMaybe(stream, state, true);
  }
}

function writeAllBuffer(stream: Writable, state: WritableState) {
  if ((state[bitfield] & (kDestroyed | kBufferProcessing | kCorked | kBuffered | kConstructed)) !==
      (kBuffered | kConstructed)) {
    return;
  }

  const objectMode = (state[bitfield] & kObjectMode) !== 0;
  const { [buffers]: buf, currentBufferIndex: bufIdx } = state;

  if (buf === undefined) {
    return;
  }

  const bufferedLength = buf.length - bufIdx;

  if (!bufferedLength) {
    return;
  }

  let i = bufIdx;

  state[bitfield] |= kBufferProcessing;
  if (bufferedLength > 1 && stream._writev) {
    state.pendingcb -= bufferedLength - 1;

    const callback = (state[bitfield] & kAllNoop) !== 0 ? undefined : (error: any) => {
      for (let n = i; n < buf.length; ++n) {
        let cb = buf[n].callback;
        if (cb) {
          cb(error);
        }
      }
    };

    const chunks = (state[bitfield] & kAllNoop) !== 0 && i === 0 ?
      buf : buf.slice(i);

    doWrite(stream, state, true, state.length, chunks, '', callback);

    clearBuffer(state);
  } else {
    do {
      const { chunk, encoding, callback } = buf[i];
      buf[i++] = null;
      const len = objectMode ? 1 : chunk.length;
      doWrite(stream, state, false, len, chunk, encoding, callback);
    } while (i < buf.length && (state[bitfield] & kWriting) === 0);

    if (i === buf.length) {
      clearBuffer(state);
    } else if (i > 256) {
      buf.splice(0, i);
      state.currentBufferIndex = 0;
    } else {
      state.currentBufferIndex = i;
    }
  }
  state[bitfield] &= ~kBufferProcessing;
}

function clearBuffer(state: WritableState): void {
  state[buffers] = undefined;
  state.currentBufferIndex = 0;
  state[bitfield] |= kAllNoop;
  state[bitfield] &= ~kBuffered;
}

class Writable extends Stream {
  // ugly implement, this may be used if the writable actually is duplex.
  _readableState: any = null;
  _writableState: WritableState;
  _writev?: Function;
  constructor() {
    super();
    this._writableState = new WritableState(this);
  }

  _write(chunk: chunk, encoding: string | null, callback: Function): void {
    if (this._writev) {
      this._writev([{chunk, encoding}], callback);
    } else {
      throw new ERR_METHOD_NOT_IMPLEMENTED('_write()');
    }
  }

  write(chunk: chunk, encoding?: string | null, callback?: Function): boolean {
    if (typeof encoding === 'function') {
      callback = encoding;
      encoding = null;
    }
    return writeImpl(this, chunk, encoding, callback) === true;
  }
}


function writeImpl(stream: Writable, chunk: chunk, encoding?: string | null, callback?: Function): Error | boolean {
  if (chunk == null) {
    throw new Error();
  }

  const state = stream._writableState;
  if ((state[bitfield] & kObjectMode)) {
    if (!encoding) {
      encoding = (state[bitfield] & kDefaultUTF8Encoding) !== 0 ? 'utf8' : state.defaultEncoding;
    } else if (encoding !== 'buffer' && !buffer.isEncoding(encoding)) {
      throw new ERR_UNKNOWN_ENCODING(encoding);
    }

    if (typeof chunk === 'string') {
      if ((state[bitfield] & kDecodeStrings) !== 0) {
        chunk = buffer.from(chunk, encoding);
        encoding = 'buffer';
      }
    } else if (chunk instanceof buffer.Buffer) {
      encoding = 'buffer';
    } else {
      try {
        chunk = buffer.from(chunk);
        encoding = 'buffer';
      } catch (e) {
        throw new Error(e);
      }
    }
  }
  // may happen only in objectMode
  if (!encoding) {
    encoding = null;
  }

  let err: Error | null = null;
  if ((state[bitfield] & kEnding) !== 0) {
    err = new ERR_STREAM_WRITE_AFTER_END();
  } else if ((state[bitfield] & kDestroyed) !== 0) {
    err = new ERR_STREAM_DESTROYED('write');
  }

  if (err) {
    if (callback !== undefined) {
      runNextTick(callback, err);
    }
    errorOrDestroy(stream, err, true);
    return err;
  }

  state.pendingcb++;
  return writeOrBuffer(stream, chunk, encoding, callback);
}

function writeOrBuffer(stream: Writable, chunk: chunk, encoding: string | null, callback?: Function): boolean {
  const state = stream._writableState;
  const len = (state[bitfield] & kObjectMode) !== 0 ? 1 : chunk.length;

  state.length += len;

  if ((state[bitfield] & (kWriting | kErrored | kCorked | kConstructed)) !== kConstructed) {
    if (!state[buffers]) {
      state[buffers] = [];
    }
    state[buffers].push({ chunk, encoding, callback });
    if ((state[bitfield] & kAllNoop) !== 0 && callback !== undefined) {
      state[bitfield] &= ~kAllNoop;
    }
  } else {
    state.currentWritelen = len;
    state.currentWritecb = callback;
    state[bitfield] |= kWriting | kSync | kExpectWriteCb;
    stream._write(chunk, encoding, state.onwrite);
    state[bitfield] &= ~kSync;
  }

  const ret = state.length < state.highWaterMark;

  if (!ret) {
    state[bitfield] |= kNeedDrain;
  }

  // Return false if errored or destroyed in order to break
  // any synchronous while(stream.write(data)) loops.
  return ret && (state[bitfield] & (kDestroyed | kErrored)) === 0;
}

export {
    Writable
};